/*
 * This file is part of Cockpit.
 *
 * Copyright (C) 2013 Red Hat, Inc.
 *
 * Cockpit is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * Cockpit is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
 */

import $ from 'jquery';
import cockpit from 'cockpit';

import React from 'react';
import ReactDOM from 'react-dom';
import { mustache } from 'mustache';
import * as authorized_keys from './authorized-keys.js';

import 'patterns';
import 'bootstrap-datepicker/dist/js/bootstrap-datepicker';
import 'form-layout.less';

const _ = cockpit.gettext;
const C_ = cockpit.gettext;

function passwd_self(old_pass, new_pass) {
    var old_exps = [
        /Current password: $/,
        /.*\(current\) UNIX password: $/,
    ];
    var new_exps = [
        /.*New password: $/,
        /.*Retype new password: $/,
        /.*Enter new \w*\s?password: $/,
        /.*Retype new \w*\s?password: $/
    ];
    var bad_exps = [
        /.*BAD PASSWORD:.*/
    ];
    var too_new_exps = [
        /.*must wait longer to change.*/
    ];

    var dfd = cockpit.defer();
    var buffer = "";
    var sent_new = false;
    var failure = _("Old password not accepted");
    var i;

    var proc;
    var timeout = window.setTimeout(function() {
        failure = _("Prompting via passwd timed out");
        proc.close("terminated");
    }, 10 * 1000);

    proc = cockpit.spawn(["/usr/bin/passwd"], { pty: true, environ: ["LC_ALL=C"], err: "out" })
            .always(function() {
                window.clearInterval(timeout);
            })
            .done(function() {
                dfd.resolve();
            })
            .fail(function(ex) {
                if (ex.exit_status)
                    ex = new Error(failure);
                dfd.reject(ex);
            })
            .stream(function(data) {
                buffer += data;
                for (i = 0; i < old_exps.length; i++) {
                    if (old_exps[i].test(buffer)) {
                        buffer = "";
                        this.input(old_pass + "\n", true);
                        return;
                    }
                }

                for (i = 0; i < too_new_exps.length; i++) {
                    if (too_new_exps[i].test(buffer)) {
                        buffer = "";
                        failure = _("You must wait longer to change your password");
                        this.input("\n", true);
                        return;
                    }
                }

                for (i = 0; i < new_exps.length; i++) {
                    if (new_exps[i].test(buffer)) {
                        buffer = "";
                        this.input(new_pass + "\n", true);
                        failure = _("Failed to change password");
                        sent_new = true;
                        return;
                    }
                }

                if (sent_new)
                    for (i = 0; i < bad_exps.length; i++) {
                        if (bad_exps[i].test(buffer)) {
                            failure = _("New password was not accepted");
                            return;
                        }
                    }
            });

    return dfd.promise();
}

function passwd_change(user, new_pass) {
    var dfd = cockpit.defer();

    cockpit.spawn(["chpasswd"], { superuser: "require", err: "out" })
            .input(user + ":" + new_pass)
            .done(function() {
                dfd.resolve();
            })
            .fail(function(ex, response) {
                if (ex.exit_status) {
                    console.log(ex);
                    if (response)
                        ex = new Error(response);
                    else
                        ex = new Error(_("Failed to change password"));
                }
                dfd.reject(ex);
            });

    return dfd.promise();
}

/*
 * Similar to $.when() but serializes, and accepts functions
 * that return promises
 */
function chain(functions) {
    var dfd = cockpit.defer();
    var i = 0;

    /* Either an array or functions passed */
    if (typeof functions == "function")
        functions = arguments;

    function step() {
        if (i == functions.length) {
            dfd.resolve();
            return;
        }

        (functions[i])()
                .done(function() {
                    step();
                })
                .fail(function(ex) {
                    dfd.reject(ex);
                });

        i += 1;
    }

    step();
    return dfd.promise();
}

function parse_passwd_content(content) {
    if (!content) {
        console.warn("Couldn't read /etc/passwd");
        return [];
    }

    var ret = [];
    var lines = content.split('\n');
    var column;

    for (var i = 0; i < lines.length; i++) {
        if (!lines[i])
            continue;
        column = lines[i].split(':');
        ret.push({
            name: column[0],
            password: column[1],
            uid: parseInt(column[2], 10),
            gid: parseInt(column[3], 10),
            gecos: (column[4] || '').replace(/,*$/, ''),
            home: column[5] || '',
            shell: column[6] || '',
        });
    }

    return ret;
}

function parse_group_content(content) {
    content = (content || "").trim();
    if (!content) {
        console.warn("Couldn't read /etc/group");
        return [];
    }

    var ret = [];
    var lines = content.split('\n');
    var column;

    for (var i = 0; i < lines.length; i++) {
        if (!lines[i])
            continue;
        column = lines[i].split(':');
        ret.push({
            name: column[0],
            password: column[1],
            gid: parseInt(column[2], 10),
            userlist: column[3].split(','),
        });
    }

    return ret;
}

function password_quality(password) {
    var dfd = cockpit.defer();

    cockpit.spawn('/usr/bin/pwscore', { err: "message" })
            .input(password)
            .done(function(content) {
                var quality = parseInt(content, 10);
                if (quality === 0) {
                    dfd.reject(new Error(_("Password is too weak")));
                } else if (quality <= 33) {
                    dfd.resolve("weak");
                } else if (quality <= 66) {
                    dfd.resolve("okay");
                } else if (quality <= 99) {
                    dfd.resolve("good");
                } else {
                    dfd.resolve("excellent");
                }
            })
            .fail(function(ex) {
                dfd.reject(new Error(ex.message || _("Password is not acceptable")));
            });

    return dfd.promise();
}

function is_user_in_group(user, group) {
    for (var i = 0; group.userlist && i < group.userlist.length; i++) {
        if (group.userlist[i] === user)
            return true;
    }

    return false;
}

class AccountItem extends React.Component {
    constructor(props) {
        super(props);
        this.click = this.click.bind(this);
    }

    click(ev) {
        if (ev && ev.button === 0)
            cockpit.location.go([this.props.name]);
    }

    render() {
        return React.createElement('div', { className: "cockpit-account", onClick: this.click },
                                   React.createElement('div', { className: "cockpit-account-pic pficon pficon-user" }),
                                   React.createElement('div', { className: "cockpit-account-real-name" }, this.props.gecos.split(',')[0]),
                                   React.createElement('a', { className: "cockpit-account-user-name", href: "#/" + this.props.name }, this.props.name)
        );
    }
}
AccountItem.displayName = 'AccountItem';

class AccountList extends React.Component {
    render() {
        var i;
        var items = [];
        for (i in this.props.accounts)
            items.push(React.createElement(AccountItem, Object.assign({ key: this.props.accounts[i].name },
                                                                      this.props.accounts[i])));
        return (
            <>
                {items}
            </>
        );
    }
}
AccountList.displayName = 'AccountList';

function log_unexpected_error(error) {
    console.warn("Unexpected error", error);
}

PageAccounts.prototype = {
    _init: function() {
        this.id = "accounts";
    },

    getTitle: function() {
        return C_("page-title", "Accounts");
    },

    show: function() {
    },

    setup: function() {
        $('#accounts-create').on('click', $.proxy(this, "create"));
    },

    enter: function() {
        var self = this;

        function parse_accounts(content) {
            self.accounts = parse_passwd_content(content);
            self.update();
        }

        this.handle_passwd = cockpit.file('/etc/passwd');

        this.handle_passwd.read()
                .done(parse_accounts)
                .fail(log_unexpected_error);

        this.handle_passwd.watch(parse_accounts);
    },

    leave: function() {
        if (this.handle_passwd) {
            this.handle_passwd.close();
            this.handle_passwd = null;
        }
    },

    update: function() {
        this.accounts.sort(function (a, b) {
            if (!a.gecos) return -1;
            else if (!b.gecos) return 1;
            else return a.gecos.localeCompare(b.gecos);
        });

        var accounts = this.accounts.filter(function(account) {
            return !((account.uid < 1000 && account.uid !== 0) ||
                account.shell.match(/^(\/usr)?\/sbin\/nologin/) ||
                account.shell === '/bin/false');
        });

        ReactDOM.render(
            React.createElement(AccountList, { accounts: accounts }),
            document.getElementById('accounts-list')
        );
    },

    create: function () {
        PageAccountsCreate.accounts = this.accounts;
        $('#accounts-create-dialog').modal('show');
    },

    go: function (user) {
        cockpit.location.go([user]);
    }
};

function PageAccounts() {
    this._init();
}

PageAccountsCreate.prototype = {
    _init: function() {
        this.id = "accounts-create-dialog";
        this.username_dirty = false;
    },

    show: function() {
    },

    setup: function() {
        var self = this;
        $('#accounts-create-cancel').on('click', $.proxy(this, "cancel"));
        $('#accounts-create-create').on('click', $.proxy(this, "create"));
        $('#accounts-create-dialog .check-passwords').on('keydown change', $.proxy(this, "validate"));
        $('#accounts-create-real-name').on('input', $.proxy(this, "suggest_username"));
        $('#accounts-create-user-name').on('input', function() { self.username_dirty = true });
    },

    enter: function() {
        $('#accounts-create-user-name').val("");
        $('#accounts-create-real-name').val("");
        $('#accounts-create-pw1').val("");
        $('#accounts-create-pw2').val("");
        $('#accounts-create-locked').prop('checked', false);
        $('#accounts-create-password-meter').removeClass("weak okay good excellent");
        $("#accounts-create-dialog").dialog("failure", null);
        this.username_dirty = false;
    },

    leave: function() {
    },

    validate: function() {
        var ex;
        var fails = [];
        var pw = $('#accounts-create-pw1').val();
        if ($('#accounts-create-pw2').val() != pw) {
            ex = new Error(_("The passwords do not match"));
            ex.target = "#accounts-create-pw2";
            fails.push(ex);
        }
        if (!$('#accounts-create-user-name').val()) {
            ex = new Error(_("No user name specified"));
            ex.target = '#accounts-create-user-name';
            fails.push(ex);
        }
        if (!$('#accounts-create-real-name').val()) {
            ex = new Error(_("No real name specified"));
            ex.target = '#accounts-create-real-name';
            fails.push(ex);
        }

        /* The first check is immediately complete */
        var dfd = cockpit.defer();
        if (fails.length)
            dfd.reject(fails);
        else
            dfd.resolve();

        var promise_password = password_quality(pw)
                .fail(function(ex) {
                    ex.target = "#accounts-create-pw2";
                })
                .always(function(arg) {
                    var strength = this.state() == "resolved" ? arg : "weak";
                    var meter = $("#accounts-create-password-meter")
                            .removeClass("weak okay good excellent");
                    if (pw)
                        meter.addClass(strength);
                    var message = $("#accounts-create-password-meter-message");
                    if (strength == "excellent") {
                        message.text(_("Excellent password"));
                    } else {
                        message.text("");
                    }
                });

        var promise_username = this.check_username()
                .fail(function(ex) {
                    ex.target = "#accounts-create-user-name";
                });

        // Can't use Promise.all() here, because this promise is passed to
        // dialog(), which expects a promise with a progress() method (see
        // pkg/lib/patterns.js)
        // eslint-disable-next-line cockpit/no-cockpit-all
        return cockpit.all(dfd.promise(), promise_password, promise_username);
    },

    cancel: function() {
        $('#accounts-create-dialog').modal('hide');
    },

    create: function() {
        var tasks = [
            () => {
                const dfd = cockpit.defer();
                cockpit.spawn(["/usr/sbin/useradd", "-D"])
                        .done(defaults => {
                            defaults.split("\n").forEach(item => {
                                if (item.indexOf("SHELL=") === 0) {
                                    this.user_shell = item.split("=")[1] || "";
                                }
                            });
                            dfd.resolve();
                        })
                        .fail(dfd.resolve); // Don't fail if we cannot read defaults
                return dfd.promise();
            },
            () => {
                var prog = ["/usr/sbin/useradd", "--create-home", "-s", this.user_shell || "/bin/bash"];
                if ($('#accounts-create-real-name').val()) {
                    prog.push('-c');
                    prog.push($('#accounts-create-real-name').val());
                }
                prog.push($('#accounts-create-user-name').val());
                return cockpit.spawn(prog, { superuser: "require", err: "message" });
            }
        ];

        tasks.push(function change_passwd() {
            return passwd_change($('#accounts-create-user-name').val(), $('#accounts-create-pw1').val());
        });

        if ($('#accounts-create-locked').prop('checked')) {
            tasks.push(function adjust_locked() {
                return cockpit.spawn([
                    "/usr/sbin/usermod",
                    $('#accounts-create-user-name').val(),
                    "--lock"
                ], { superuser: "require", err: "message" });
            });
        }

        var promise = this.validate()
                .fail(function(ex) {
                    $("#accounts-create-password-meter-message").hide();
                    $("#accounts-create-dialog").dialog("failure", ex);
                })
                .done(function() {
                    promise = chain(tasks);
                    $("#accounts-create-dialog").dialog("promise", promise);
                });

        $("#accounts-create-dialog").dialog("wait", promise);
    },

    is_valid_char_username: function(c) {
        return (c >= 'a' && c <= 'z') ||
            (c >= 'A' && c <= 'Z') ||
            (c >= '0' && c <= '9') ||
            c == '.' || c == '_' || c == '-';
    },

    check_username: function() {
        var dfd = cockpit.defer();
        var username = $('#accounts-create-user-name').val();

        for (var i = 0; i < username.length; i++) {
            if (!this.is_valid_char_username(username[i])) {
                dfd.reject(new Error(
                    _("The user name can only consist of letters from a-z, digits, dots, dashes and underscores.")
                ));
                return dfd.promise();
            }
        }

        for (var k = 0; k < PageAccountsCreate.accounts.length; k++) {
            if (PageAccountsCreate.accounts[k].name == username) {
                dfd.reject(new Error(_("This user name already exists")));
                return dfd.promise();
            }
        }

        dfd.resolve();
        return dfd.promise();
    },

    suggest_username: function() {
        var self = this;

        function remove_diacritics(str) {
            var translate_table = {
                a :  '[àáâãäå]',
                ae:  'æ',
                c :  '[čç]',
                d :  'ď',
                e :  '[èéêë]',
                i :  '[íìïî]',
                l :  '[ĺľ]',
                n :  '[ňñ]',
                o :  '[òóôõö]',
                oe:  'œ',
                r :  '[ŕř]',
                s :  'š',
                t :  'ť',
                u :  '[ùúůûűü]',
                y :  '[ýÿ]',
                z :  'ž',
            };
            for (var i in translate_table)
                str = str.replace(new RegExp(translate_table[i], 'g'), i);

            for (var k = 0; k < str.length;) {
                if (!self.is_valid_char_username(str[k]))
                    str = str.substr(0, k) + str.substr(k + 1);
                else
                    k++;
            }

            return str;
        }

        function make_username(realname) {
            var result = "";
            var name = realname.split(' ');

            if (name.length === 1)
                result = name[0].toLowerCase();
            else if (name.length > 1)
                result = name[0][0].toLowerCase() + name[name.length - 1].toLowerCase();

            return remove_diacritics(result);
        }

        if (this.username_dirty)
            return;

        var username = make_username($('#accounts-create-real-name').val());
        $('#accounts-create-user-name').val(username);
    }

};

function PageAccountsCreate() {
    this._init();
}

PageAccount.prototype = {
    _init: function(user) {
        this.id = "account-page";
        this.section_id = "accounts";
        this.roles = [];
        this.role_template = $("#role-entry-tmpl").html();
        mustache.parse(this.role_template);

        this.keys_template = $("#authorized-keys-tmpl").html();
        mustache.parse(this.keys_template);
        this.authorized_keys = null;

        this.user = user;

        this.permission = cockpit.permission({ admin: true });
        this.permission.addEventListener("changed", () => this.update_accounts_privileged());
    },

    getTitle: function() {
        return C_("page-title", "Accounts");
    },

    update_accounts_privileged: function() {
        $(".accounts-self-privileged").addClass("accounts-privileged");

        $(".accounts-privileged:not('.accounts-current-account')").update_privileged(
            this.permission, cockpit.format(
                _("The user <b>$0</b> is not permitted to modify accounts"),
                this.permission.user ? this.permission.user.name : '')
        );
        $(".accounts-privileged").find("input")
                .attr('disabled', this.permission.allowed === false);

        // not exactly a permission, but needs to be adjusted after the above
        if (this.permission.allowed && !this.logged)
            $('#account-logout').attr('disabled', true);

        // enable fields for current account.
        $(".accounts-current-account").update_privileged(
            { allowed: true }, ""
        );
        $(".accounts-current-account").find("input")
                .attr('disabled', false);

        if ($('#account-user-name').text() === 'root' && this.permission.allowed) {
            $("#account-delete").update_privileged({ allowed: false },
                                                   _("Unable to delete root account"));
            $("#account-real-name-wrapper").update_privileged({ allowed: false },
                                                              _("Unable to rename root account"));
            $("#account-real-name").prop('disabled', true);
        }
    },

    show: function() {
        var self = this;
        $("#account").toggle(!!self.account_id);
        $("#account-failure").toggle(!self.account_id);
    },

    setup: function() {
        $('#account .breadcrumb a').on("click", function() {
            cockpit.location.go('/');
        });

        $('#account-real-name').on('change', $.proxy(this, "change_real_name"));
        $('#account-real-name').on('keydown', $.proxy(this, "real_name_edited"));
        $('#account-set-password').on('click', $.proxy(this, "set_password"));
        $('#account-delete').on('click', $.proxy(this, "delete_account"));
        $('#account-logout').on('click', $.proxy(this, "logout_account"));
        $('#account-locked').on('change', $.proxy(this, "change_locked", true, null));
        $('#add-authorized-key').on('click', $.proxy(this, "add_key"));
        $('#add-authorized-key-dialog').on('hidden.bs.modal', function () {
            $("#authorized-keys-text").val("");
        });
    },

    setup_keys: function (user_name, home_dir) {
        var self = this;
        if (!self.authorized_keys) {
            self.authorized_keys = authorized_keys.instance(user_name, home_dir);
            $(self.authorized_keys).on("changed", function () {
                self.update();
            });
        }
    },

    remove_key: function (ev) {
        if (!this.authorized_keys)
            return;

        var key = $(ev.target).data("raw");
        $(".account-remove-key").prop('disabled', true);
        this.authorized_keys.remove_key(key)
                .fail(show_unexpected_error)
                .always(function () {
                    $(".account-remove-key").prop('disabled', false);
                });
    },

    add_key: function () {
        if (!this.authorized_keys) {
            $("#add-authorized-key-dialog").modal('hide');
            return;
        }

        var key = $("#authorized-keys-text").val();
        var promise = this.authorized_keys.add_key(key);
        $("#add-authorized-key-dialog").dialog("promise", promise);
    },

    get_user: function() {
        var self = this;
        function parse_user(content) {
            var accounts = parse_passwd_content(content);

            for (var i = 0; i < accounts.length; i++) {
                if (accounts[i].name !== self.account_id)
                    continue;

                self.account = accounts[i];
                self.setup_keys(self.account.name, self.account.home);
                self.update();
                return;
            }

            /* no such account find, clear it */
            self.account = null;
        }

        this.handle_passwd = cockpit.file('/etc/passwd');
        self.handle_shadow = cockpit.file('/etc/shadow', { superuser: "try" });
        var saw_shadow = false;

        this.handle_passwd.read()
                .done(parse_user)
                .fail(log_unexpected_error);

        this.handle_passwd.watch(function(content) {
            parse_user(content);
            if (!saw_shadow)
                self.get_expire();
            if (self.account)
                self.get_locked();
        });
        self.handle_shadow.watch(function() {
            saw_shadow = true;
            self.get_expire();
        });
    },

    get_roles: function() {
        var self = this;

        var role_groups = {
            wheel:   _("Server Administrator"),
            sudo:    _("Server Administrator"),
            docker:  _("Container Administrator"),
            weldr:   _("Image Builder")
        };

        function parse_groups(content) {
            var groups = parse_group_content(content);
            while (self.roles.length > 0)
                self.roles.pop();
            for (var i = 0; i < groups.length; i++) {
                var name = groups[i].name;
                if (role_groups[name]) {
                    self.roles.push({
                        name: name,
                        desc: role_groups[name],
                        id: groups[i].gid,
                        member: is_user_in_group(self.account_id, groups[i]),
                    });
                }
            }
            $(self).triggerHandler("roles");
            self.update();
        }

        this.handle_groups = cockpit.file('/etc/group');

        this.handle_groups.read()
                .done(parse_groups)
                .fail(log_unexpected_error);

        this.handle_groups.watch(parse_groups);
    },

    get_last_login: function() {
        var self = this;

        function parse_last_login(data) {
            data = data.split('\n')[1]; // throw away header
            if (data.length === 0) return null;
            data = data.split('   '); // get last column - separated by spaces

            if (data[data.length - 1].indexOf('**Never logged in**') > -1)
                return null;
            else
                return new Date(data[data.length - 1]);
        }

        cockpit.spawn(["/usr/bin/lastlog", "-u", self.account_id], { environ: ["LC_ALL=C"] })
                .done(function (data) {
                    self.lastLogin = parse_last_login(data);
                    self.update();
                })
                .fail(function() {
                    self.lastLogin = null;
                    self.update();
                });
    },

    get_locked: function(update_display) {
        update_display = typeof update_display !== 'undefined' ? update_display : true;
        var dfd = cockpit.defer();
        var self = this;

        function parse_locked(content) {
            var status = content.split(" ")[1];
            // libuser uses "LK", shadow-utils use "L".
            return status && (status == "LK" || status == "L");
        }

        cockpit.spawn(["/usr/bin/passwd", "-S", self.account_id], { environ: ["LC_ALL=C"], superuser: "require" })
                .done(function(content) {
                    self.locked = parse_locked(content);
                    if (update_display)
                        self.update();
                    dfd.resolve(self.locked);
                })
                .fail(function(error) {
                    dfd.reject(error);
                });

        return dfd.promise();
    },

    get_logged: function() {
        var self = this;
        if (!self.account_id) {
            self.logged = false;
            self.update();
            return;
        }

        function parse_logged(content) {
            self.logged = content.length > 0;
            if (!self.logged)
                self.get_last_login();
            else
                self.update();
        }

        cockpit.spawn(["/usr/bin/w", "-sh", self.account_id])
                .done(parse_logged)
                .fail(log_unexpected_error);
    },

    get_expire: function() {
        var self = this;

        function parse_expire(data) {
            var i, line;
            data = data.split('\n');

            var account_expiration = '';
            var account_date = '';

            var password_expiration = '';
            var password_days = -1;

            for (i = 0; i < data.length; i++) {
                line = data[i].split(': ');
                if (line[0] && line[0].indexOf("Password expires") === 0) {
                    if (line[1].indexOf("never") === 0) {
                        password_expiration = _("Never expire password");
                    } else if (line[1].indexOf("password must be changed") === 0) {
                        password_expiration = _("Password must be changed");
                    } else {
                        password_expiration = cockpit.format(_("Require password change on $0"), line[1]);
                    }
                } else if (line[0] && line[0].indexOf("Account expires") === 0) {
                    if (line[1].indexOf("never") === 0) {
                        account_expiration = _("Never lock account");
                    } else {
                        account_date = new Date(line[1] + " 12:00:00 UTC");
                        account_expiration = cockpit.format(_("Lock account on $0"), line[1]);
                    }
                } else if (line[0] && line[0].indexOf("Maximum number of days between password change") === 0) {
                    password_days = line[1];
                }
            }

            $('#account-expiration-button').text(account_expiration);
            $('#account-expiration-button').data('expire-date', account_date);

            $('#password-expiration-button').text(password_expiration);
            $('#password-expiration-button').data('expire-days', password_days);

            self.update();
        }

        cockpit.spawn(["/usr/bin/chage", "-l", self.account_id],
                      { environ: ["LC_ALL=C"], err: "message", superuser: "try" })
                .done(function(data) {
                    parse_expire(data);
                })
                .fail(function(ex) {
                    parse_expire("");
                });
    },

    enter: function(account_id) {
        this.account_id = account_id;

        $("#account-real-name").removeAttr("data-dirty");
        $('#password-reset-button').data('account-id', this.account_id);
        $('#password-expiration-button').data('account-id', this.account_id);
        $('#account-expiration-button').data('account-id', this.account_id);

        this.get_user();
        this.get_roles();
        this.get_locked();
        this.get_logged();
        this.get_expire();
    },

    leave: function() {
        if (this.handle_passwd) {
            this.handle_passwd.close();
            this.handle_passwd = null;
        }

        if (this.handle_groups) {
            this.handle_groups.close();
            this.handle_groups = null;
        }

        if (this.authorized_keys) {
            $(this.authorized_keys).off();
            this.authorized_keys.close();
            this.authorized_keys = null;
        }

        $('#account-failure').prop("hidden", true);
    },

    update: function() {
        if (this.account) {
            $('#account').prop("hidden", false);
            $('#account-failure').prop("hidden", true);
            var name = $("#account-real-name");

            var title_name = this.account.gecos;
            if (title_name)
                title_name = title_name.split(',')[0];
            else
                title_name = this.account.name;

            $("#account-title").text(title_name);
            if (!name.attr("data-dirty"))
                $('#account-real-name').val(this.account.gecos);

            $('#account-user-name').text(this.account.name);

            if (this.logged)
                $('#account-last-login').text(_("Logged In"));
            else if (!this.lastLogin)
                $('#account-last-login').text(_("Never"));
            else
                $('#account-last-login').text(this.lastLogin.toLocaleString());

            if (typeof this.locked != 'undefined') {
                $('#account-locked').prop('checked', this.locked);
                $('#account-locked').prop('disabled', false);
            } else {
                $('#account-locked').prop('disabled', true);
            }

            if (this.authorized_keys) {
                var keys = this.authorized_keys.keys;
                var state = this.authorized_keys.state;
                var keys_html = mustache.render(this.keys_template, {
                    keys: keys,
                    empty: keys.length === 0 && state == "ready",
                    denied: state == "access-denied",
                    failed: state == "failed",
                });
                $('#account-authorized-keys-list').html(keys_html);
                $(".account-remove-key")
                        .on("click", $.proxy(this, "remove_key"));
                $('#account-authorized-keys').show();
            } else {
                $('#account-authorized-keys').hide();
            }

            if (this.account.uid !== 0) {
                var html = mustache.render(this.role_template,
                                           { roles: this.roles, changed: this.roles_changed });
                $('#account-change-roles-roles').html(html);
                $('#account-roles').parents('tr')
                        .show();
                $('#account-roles [data-toggle="tooltip"]')
                        .tooltip();
                $("#account-change-roles-roles :input")
                        .on("change", $.proxy(this, "change_role"));
            } else {
                $('#account-roles')
                        .parents('tr')
                        .hide();
            }
            $('#account .breadcrumb .active')
                    .text(title_name);

            // check accounts-self-privileged whether account is the same as currently logged in user
            $(".accounts-self-privileged")
                    .toggleClass("accounts-current-account",
                                 this.user.id == this.account.uid);
        } else {
            $('#account').hide();
            $('#account-failure').prop("hidden", false);
            $('#account-real-name').val("");
            $('#account-user-name').text("");
            $('#account-last-login').text("");
            $('#account-locked').prop('checked', false);
            $('#account-change-roles-roles').html("");
            $('#account .breadcrumb .active').text("?");
        }
        this.update_accounts_privileged();
    },

    change_role: function(ev) {
        var self = this;
        var name = $(ev.target).data("name");
        var id = $(ev.target).data("gid");
        if (!name || !id || !this.account.name)
            return;

        var proc;
        var checked = $(ev.target).prop('checked');
        var input_elements =
            $('#account button:not([disabled]), #account input:not([disabled]), #account a:not([disabled])');
        input_elements.prop('disabled', true);
        if (checked) {
            proc = cockpit.spawn(["/usr/sbin/usermod", this.account.name, "-G", id, "-a"],
                                 { superuser: "require", err: "message" });
        } else {
            proc = cockpit.spawn(["/usr/bin/gpasswd", "-d", this.account.name, name],
                                 { superuser: "require", err: "message" });
        }

        proc.then(function(data) {
            if (!data && checked)
                data = "Added " + self.account.name + " to group " + name;
            else if (!data && !checked)
                data = "Removed " + self.account.name + " from group " + name;
            console.log(data);
            self.roles_changed = true;
            self.update();
        }, show_unexpected_error).finally(function() {
            input_elements.prop('disabled', false);
        });
    },

    real_name_edited: function() {
        $("#account-real-name").attr("data-dirty", "true");
    },

    check_role_for_self_mod: function () {
        return (this.account.name == this.user.name || !!this.permission.allowed);
    },

    change_real_name: function() {
        var self = this;

        var name = $("#account-real-name");
        name.attr("data-dirty", "true");

        if (!self.check_role_for_self_mod()) {
            self.update();
            return;
        }

        // TODO: unwanted chars check
        var value = name.val();
        var input_elements =
            $('#account button:not([disabled]), #account input:not([disabled]), #account a:not([disabled])');
        input_elements.prop('disabled', true);
        cockpit.spawn(["/usr/sbin/usermod", self.account.name, "--comment", value],
                      { superuser: "try", err: "message" })
                .done(function(data) {
                    self.account.gecos = value;
                    self.update();
                    name.removeAttr("data-dirty");
                })
                .fail(show_unexpected_error)
                .finally(function() {
                    input_elements.prop('disabled', false);
                });
    },

    change_locked: function(verify_status, desired_lock_state) {
        desired_lock_state = desired_lock_state !== null
            ? desired_lock_state : $('#account-locked').prop('checked');
        var self = this;
        var input_elements =
            $('#account button:not([disabled]), #account input:not([disabled]), #account a:not([disabled])');
        input_elements.prop('disabled', true);
        cockpit.spawn(["/usr/sbin/usermod",
            this.account.name,
            desired_lock_state ? "--lock" : "--unlock"], { superuser: "require", err: "message" })
                .done(function() {
                    self.get_locked(false)
                            .done(function(locked) {
                                /* if we care about what the lock state should be and it doesn't match, try to change again
                                   this is a workaround for different ways of handling a locked account
                                   https://github.com/cockpit-project/cockpit/issues/1216
                                   https://bugzilla.redhat.com/show_bug.cgi?id=853153
                                   This seems to be fixed in fedora 23 (usermod catches the different locking behavior)
                                   */
                                if (verify_status && desired_lock_state !== locked) {
                                    console.log("Account locked state doesn't match desired value, trying again.");
                                    // only retry once to avoid uncontrolled recursion
                                    self.change_locked(false, desired_lock_state);
                                } else {
                                    self.update();
                                }
                            });
                })
                .fail(show_unexpected_error)
                .finally(function() {
                    input_elements.prop('disabled', false);
                });
    },

    set_password: function() {
        if (!this.check_role_for_self_mod())
            return;

        PageAccountSetPassword.user_name = this.account.name;
        /* TODO: get rid of this once monitoring /etc/shadow will be implemented */
        PageAccountSetPassword.update_callback = $.proxy(this, "enter");
        $('#account-set-password-dialog').modal('show');
    },

    delete_account: function() {
        PageAccountConfirmDelete.user_name = this.account.name;
        $('#account-confirm-delete-dialog').modal('show');
    },

    logout_account: function() {
        var input_elements =
            $('#account button:not([disabled]), #account input:not([disabled]), #account a:not([disabled])');
        input_elements
                .prop('disabled', true);
        cockpit.spawn(["/usr/bin/loginctl", "terminate-user", this.account.name],
                      { superuser: "try", err: "message" })
                .done($.proxy(this, "get_logged"))
                .fail(show_unexpected_error)
                .finally(function() {
                    input_elements.prop('disabled', false);
                });
    },
};

function PageAccount(user) {
    this._init(user);
}

PageAccountConfirmDelete.prototype = {
    _init: function() {
        this.id = "account-confirm-delete-dialog";
    },

    show: function() {
    },

    setup: function() {
        $('#account-confirm-delete-apply').on('click', $.proxy(this, "apply"));
    },

    enter: function() {
        $('#account-confirm-delete-files').prop('checked', false);
        $('#account-confirm-delete-title').text(cockpit.format(_("Delete $0"), PageAccountConfirmDelete.user_name));
    },

    leave: function() {
    },

    apply: function() {
        var prog = ["/usr/sbin/userdel"];

        if ($('#account-confirm-delete-files').prop('checked'))
            prog.push("-r");

        prog.push(PageAccountConfirmDelete.user_name);

        cockpit.spawn(prog, { superuser: "require", err: "message" })
                .done(function () {
                    $('#account-confirm-delete-dialog').modal('hide');
                    cockpit.location.go("/");
                })
                .fail(show_unexpected_error);
    }
};

function PageAccountConfirmDelete() {
    this._init();
}

function AccountExpiration() {
    $("#account-expiration").on("show.bs.modal", function(ev) {
        var account_id = $(ev.relatedTarget).data("account-id");
        $("#account-expiration").data("account-id", account_id);

        /* Fill in initial dialog values */
        var expire_date = $(ev.relatedTarget).data("expire-date");
        $(expire_date ? '#account-expiration-expires' : "#account-expiration-never").prop('checked', true);
        $('#account-expiration-input').val(expire_date ? expire_date.toISOString().substr(0, 10) : "");
        $('#account-expiration-input').prop('disabled', !expire_date);

        /* TRANSLATORS: This is split up and therefore cannot use ngettext plurals */
        var parts = _("Lock account on $0").split("$0");
        $("#account-expiration-before").text(parts[0]);
        $("#account-expiration-after").text(parts[1]);
    });

    $('#account-expiration .btn-primary').on('click', function() {
        var date, value;
        var ex;
        var promise = null;

        /* Parse the dialog data and validate */
        if ($('#account-expiration-expires').prop('checked')) {
            value = $('#account-expiration-input').val();
            if (!value) {
                ex = new Error(_("Please specify an expiration date"));
                ex.target = "#account-expiration-input";
            } else {
                date = new Date(value + "T12:00:00Z");
                if (isNaN(date.getTime()) || date.getTime() < 0) {
                    ex = new Error(_("Invalid expiration date"));
                    ex.target = "#account-expiration-input";
                }
            }
            if (ex)
                promise = cockpit.reject(ex);
        }

        if ($('#account-expiration-never').prop('checked')) {
            date = null;
            promise = null;
        }

        /* Actually perform the action if valid */
        var prog = ["/usr/sbin/usermod", "-e"];
        var account_id = $("#account-expiration").data("account-id");
        if (!promise) {
            if (date)
                prog.push(date.toISOString().substr(0, 10));
            else
                prog.push("");
            prog.push(account_id);
            promise = cockpit.spawn(prog, { superuser : true, err: "message" });
        }

        $("#account-expiration").dialog("promise", promise);
    });

    $('#account-expiration input').on('change', function() {
        $('#account-expiration-input').prop('disabled', $("#account-expiration-never").prop('checked'));
    });

    $('#account-expiration-input').datepicker({
        autoclose: true,
        todayHighlight: true,
        format: 'yyyy-mm-dd'
    });

    $('#account-expiration-input').on("show.bs.modal", function(ev) {
        ev.stopPropagation();
    });
}

function PasswordExpiration() {
    var never = 99999;

    $("#password-expiration").on("show.bs.modal", function(ev) {
        var account_id = $(ev.relatedTarget).data("account-id");
        $("#password-expiration").data("account-id", account_id);

        /* Fill in initial dialog values */
        var expire_days = parseInt($(ev.relatedTarget).data("expire-days"), 10);
        if (isNaN(expire_days) || expire_days < 0 || expire_days >= never)
            expire_days = null;
        $(expire_days ? '#password-expiration-expires' : "#password-expiration-never").prop('checked', true);
        $('#password-expiration-input').val(expire_days || "");
        $('#password-expiration-input').prop('disabled', !expire_days);

        /* TRANSLATORS: This is split up and therefore cannot use ngettext plurals */
        var parts = _("Require password change every $0 days").split("$0");
        $("#password-expiration-before").text(parts[0]);
        $("#password-expiration-after").text(parts[1]);
    });

    $('#password-expiration .btn-primary').on('click', function() {
        var days, ex;
        var promise = null;

        if ($('#password-expiration-expires').prop('checked'))
            days = parseInt($('#password-expiration-input')
                    .val()
                    .trim(), 10);
        if ($('#password-expiration-never').prop('checked'))
            days = never;

        if (isNaN(days) || days < 0) {
            ex = new Error(_("Invalid number of days"));
            ex.target = "#password-expiration-input";
            promise = cockpit.reject(ex);
        }

        var account_id = $("#password-expiration").data("account-id");

        if (!promise) {
            promise = cockpit.spawn(["/usr/bin/passwd", "-x", String(days), account_id],
                                    { superuser: true, err: "message" });
        }

        $("#password-expiration").dialog("promise", promise);
    });

    $('#password-expiration input').on('change', function() {
        $('#password-expiration-input').prop('disabled', $("#password-expiration-never").prop('checked'));
    });
}

function PasswordReset() {
    $("#password-reset").on("show.bs.modal", function(ev) {
        var account_id = $(ev.relatedTarget).data("account-id");
        $("#password-reset").data("account-id", account_id);

        var msg = cockpit.format(_("The account '$0' will be forced to change their password on next login"),
                                 account_id);
        $("#password-reset .modal-body p").text(msg);
    });

    $("#password-reset .btn-primary").on("click", function() {
        var account_id = $("#password-reset").data("account-id");
        var promise = cockpit.spawn(["/usr/bin/passwd", "-e", account_id],
                                    { superuser : true, err: "message" });
        $("#password-reset").dialog("promise", promise);
    });
}

PageAccountSetPassword.prototype = {
    _init: function(user) {
        this.id = "account-set-password-dialog";
        this.user = user;
    },

    show: function() {
        if (this.user.id === 0 || this.user.name !== PageAccountSetPassword.user_name) {
            $('#account-set-password-old')
                    .toggle(false);
            $('#account-set-password-old').prev()
                    .toggle(false);
            $('#account-set-password-pw1')
                    .focus();
        } else {
            $('#account-set-password-old')
                    .toggle(true);
            $('#account-set-password-old').prev()
                    .toggle(true);
            $('#account-set-password-old')
                    .focus();
        }
    },

    setup: function() {
        $('#account-set-password-apply').on('click', $.proxy(this, "apply"));
        $('#account-set-password-dialog .check-passwords').on('keydown change', $.proxy(this, "validate"));
    },

    enter: function() {
        $('#account-set-password-old').val("");
        $('#account-set-password-pw1').val("");
        $('#account-set-password-pw2').val("");
        $('#account-set-password-meter').removeClass("weak okay good excellent");
        $("#account-set-password-dialog").dialog("failure", null);
    },

    leave: function() {
    },

    validate: function() {
        var ex;
        var pw = $('#account-set-password-pw1').val();
        if ($('#account-set-password-pw2').val() != pw) {
            ex = new Error(_("The passwords do not match"));
            ex.target = "#account-set-password-pw2";
        }

        var dfd = cockpit.defer();
        if (ex)
            dfd.reject(ex);
        else
            dfd.resolve();

        var promise = password_quality(pw)
                .fail(function(ex) {
                    ex.target = "#account-set-password-pw2";
                })
                .always(function(arg) {
                    var strength = (this.state() == "resolved") ? arg : "weak";
                    var meter = $("#account-set-password-meter")
                            .removeClass("weak okay good excellent");
                    if (pw)
                        meter.addClass(strength);
                    var message = $("#account-set-password-meter-message");
                    if (strength == "excellent") {
                        message.text(_("Excellent password"));
                    } else {
                        message.text("");
                    }
                });

        // Can't use Promise.all() here, because this promise is passed to
        // dialog(), which expects a promise with a progress() method (see
        // pkg/lib/patterns.js)
        // eslint-disable-next-line cockpit/no-cockpit-all
        return cockpit.all(dfd.promise(), promise);
    },

    apply: function() {
        var self = this;

        var promise = this.validate()
                .done(function() {
                    var user = PageAccountSetPassword.user_name;
                    var password = $('#account-set-password-pw1').val();

                    if (self.user.name === user)
                        promise = passwd_self($('#account-set-password-old').val(), password);
                    else
                        promise = passwd_change(user, password);

                    $("#account-set-password-dialog").dialog("promise", promise);
                })
                .fail(function(ex) {
                    $("#account-set-password-meter-message").prop("hidden", true);
                    $("#account-set-password-dialog").dialog("failure", ex);
                });
        $("#account-set-password-dialog").dialog("wait", promise);
    }
};

function PageAccountSetPassword(user) {
    this._init(user);
}

/* INITIALIZATION AND NAVIGATION
 *
 * The code above still uses the legacy 'Page' abstraction for both
 * pages and dialogs, and expects page.setup, page.enter, page.show,
 * and page.leave to be called at the right times.
 *
 * We cater to this with a little compatability shim consisting of
 * 'dialog_setup', 'page_show', and 'page_hide'.
 */

function show_error_dialog(title, message) {
    if (message) {
        $("#error-popup-title").text(title);
        $("#error-popup-message").text(message);
    } else {
        $("#error-popup-title").text(_("Error"));
        $("#error-popup-message").text(title);
    }

    $('.modal[role="dialog"]').modal('hide');
    $('#error-popup').modal('show');
}

function show_unexpected_error(error) {
    show_error_dialog(_("Unexpected error"), error.message || error);
}

function dialog_setup(d) {
    d.setup();
    $('#' + d.id)
            .on('show.bs.modal', function () { d.enter() })
            .on('shown.bs.modal', function () { d.show() })
            .on('hidden.bs.modal', function () { d.leave() });
}

function page_show(p, arg) {
    if (p._entered_)
        p.leave();
    p.enter(arg);
    p._entered_ = true;
    $('#' + p.id).prop("hidden", false);
    p.show();
}

function page_hide(p) {
    $('#' + p.id).prop("hidden", true);
    if (p._entered_) {
        p.leave();
        p._entered_ = false;
    }
}

function init() {
    var overview_page;
    var account_page;

    cockpit.user().done(function (user) {
        function navigate() {
            var path = cockpit.location.path;

            if (path.length === 0) {
                page_hide(account_page);
                page_show(overview_page);
            } else if (path.length === 1) {
                page_hide(overview_page);
                page_show(account_page, path[0]);
            } else { /* redirect */
                console.warn("not a users location: " + path);
                cockpit.location = '';
            }

            $("body").prop("hidden", false);
        }

        cockpit.translate();

        overview_page = new PageAccounts();
        overview_page.setup();

        account_page = new PageAccount(user);
        account_page.setup();

        dialog_setup(new PageAccountsCreate());
        dialog_setup(new PageAccountConfirmDelete());
        dialog_setup(new PageAccountSetPassword(user));

        AccountExpiration();
        PasswordExpiration();
        PasswordReset();

        $(cockpit).on("locationchanged", navigate);
        navigate();
    });
}

$(init);
